package me.softwareengineer.guice.rendering.domain.referenece;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import me.softwareengineer.guice.rendering.domain.HasMetaData;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

/**
 * Represents a reference to a Resource (document, image, attachment, mail,
 * etc).
 * 
 * @author yadtahir
 * 
 */
public class ResourceReference implements HasMetaData {

	/**
	 * Holds metadata information
	 */
	private Map<String, Object> metadata = new LinkedHashMap<String, Object>();

	/**
	 * true if the resource type has been explicitly provided (eg in XWiki
	 * Syntax 2.1 if the reference is prefixed with the resource type followed
	 * by ":" and then the rest of the reference)
	 */
	private boolean isExplicitlyProvided = true;

	/**
	 * Holds the reference of the resource
	 */
	private String reference;

	/**
	 * Holds the type of the resource
	 */
	private ResourceType type;

	/**
	 * Bracket start char.
	 */
	private static final char BRACKET_START = '[';

	/**
	 * Bracket dtop char.
	 */
	private static final char BRACKET_STOP = ']';

	/**
	 * Comma followed by space.
	 */
	private static final String COMMA_SPACE = ", ";

	/**
	 * Creates an resource reference without metadata.
	 * 
	 * @param type
	 *            the type of the resource
	 * @param reference
	 *            the reference of the resource
	 */
	public ResourceReference(String reference, ResourceType type) {
		this.type = type;
		this.reference = reference;
	}

	/**
	 * Creates ResourceReferenece with the given metadata records
	 * 
	 * @param metaData
	 */
	public ResourceReference(String reference, ResourceType type,
			Map<String, Object> metaData) {
		this.metadata.putAll(metaData);
		this.type = type;
		this.reference = reference;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void addMetaData(HasMetaData metaData) {
		for (Map.Entry<String, Object> entry : metaData.getAllMetaData()
				.entrySet()) {
			addMetaData(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void addMetaData(Map<String, Object> records) {
		this.metadata.putAll(records);
	}

	/**
	 * Resource references to be extensible we allow for extra parameters in
	 * addition to the Resource reference. For example this is used in Document
	 * Resource References for storing the query string and anchor information,
	 * and in InterWiki Resource References to store the InterWiki Alias. Note
	 * that supported parameters depend on the Renderer that will be used (i.e.
	 * it depends on the target Syntax). For example the XWiki Syntax 2.1 only
	 * supports "queryString" and "anchor".
	 * 
	 * @param key
	 *            the key reference of the metadata
	 * @param value
	 *            the value of the metadata
	 */
	@Override
	public void addMetaData(String key, Object value) {
		this.metadata.put(key, value);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see Object#clone()
	 */
	@Override
	public ResourceReference clone() {
		ResourceReference clone;
		try {
			clone = (ResourceReference) super.clone();
		} catch (CloneNotSupportedException e) {
			// Should never happen
			throw new RuntimeException("Failed to clone object", e);
		}
		return clone;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean containsMetadata(String key) {
		return this.metadata.containsKey(key);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see Object#equals(Object)
	 */
	@Override
	public boolean equals(Object object) {
		if (object == null) {
			return false;
		}
		if (object == this) {
			return true;
		}
		if (object.getClass() != getClass()) {
			return false;
		}
		ResourceReference rhs = (ResourceReference) object;
		return new EqualsBuilder().append(getType(), rhs.getType())
				.append(isExplicitlyProvided(), rhs.isExplicitlyProvided())
				.append(getReference(), rhs.getReference())
				.append(getAllMetaData(), rhs.getAllMetaData()).isEquals();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Map<String, Object> getAllMetaData() {
		return Collections.unmodifiableMap(this.metadata);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object getMetaData(String key) {
		return this.metadata.get(key);
	}

	/**
	 * 
	 * @return the reference of the resource
	 */
	public String getReference() {
		return reference;
	}

	/**
	 * 
	 * @return the type of the resource
	 * @see ResourceType
	 */
	public ResourceType getType() {
		return type;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return new HashCodeBuilder(1, 9).append(getType())
				.append(isExplicitlyProvided()).append(getReference())
				.append(getAllMetaData()).toHashCode();
	}

	/**
	 * 
	 * @return true if the resource type has been explicitly provided (eg in
	 *         XWiki Syntax 2.1 if the reference is prefixed with the resource
	 *         type followed by ":" and then the rest of the reference)
	 */
	public boolean isExplicitlyProvided() {
		return isExplicitlyProvided;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removeMetaData(String key) {
		if (containsMetadata(key))
			this.metadata.remove(key);

	}

	/**
	 * Sets whether the reference information is explicitly provided. See
	 * {@link #isExplicitlyProvided()}
	 * 
	 * @param isProvided
	 */
	public void setExplicitlyProvided(boolean isProvided) {
		this.isExplicitlyProvided = isProvided;
	}

	/**
	 * Sets the reference of the resource
	 * 
	 * @param reference
	 */
	public void setReference(String reference) {
		this.reference = reference;
	}

	/**
	 * Sets the type of resource
	 * 
	 * @param type
	 *            the resource type
	 * @see ResourceType
	 */
	public void setType(ResourceType type) {
		this.type = type;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * The output is syntax independent since this class is used for all
	 * syntaxes. Specific syntaxes should extend this class and override this
	 * method to perform syntax-dependent formatting.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("Typed = [").append(isExplicitlyProvided()).append("]");
		sb.append(" ");
		sb.append("Type = [").append(getType().toString()).append("]");
		if (getReference() != null) {
			sb.append(" ");
			sb.append("Reference = [").append(getReference()).append("]");
		}

		Map<String, Object> params = getAllMetaData();
		if (!params.isEmpty()) {
			sb.append(" ");
			sb.append("Parameters = [");
			Iterator<Map.Entry<String, Object>> it = params.entrySet()
					.iterator();
			while (it.hasNext()) {
				Map.Entry<String, Object> entry = it.next();
				sb.append(BRACKET_START).append(entry.getKey()).append("] = [")
						.append(entry.getValue());
				sb.append(BRACKET_STOP);
				if (it.hasNext()) {
					sb.append(COMMA_SPACE);
				}
			}
			sb.append(BRACKET_STOP);
		}

		return sb.toString();
	}
}
